package redislock

import (
	"codeup.aliyun.com/5f9118049cffa29cfdd3be1c/util"
	"context"
	"errors"
	"github.com/redis/go-redis/v9"
	"time"
)

// UnlockScript 解锁脚本
const unlockScriptLua = `
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end`

var unlockScript = redis.NewScript(unlockScriptLua)

// Mutex 分布式锁
type Mutex struct {
	// client redis客户端
	client *redis.Client

	// key 锁的key
	key string

	// spinInterval 自旋获取锁的间隔时间
	spinInterval time.Duration

	// 每次续期的时间
	watchDogExpire time.Duration

	// 最大续期次数，0表示无限次
	watchDogMaxTimes int

	log util.Logger

	// 是否启用本地锁
	isOpenLocalLock bool
}

// WithIsOpenLocalLockOption 开启使用本地锁预先处理，开启后，会先取本地锁，再取redis，防止大量请示到redis
func WithIsOpenLocalLockOption() withLockOption {
	return func(d *Mutex) {
		d.isOpenLocalLock = true
	}
}

// WithWatchDogExpireOption 设置看门狗续期时间
// 支持到毫秒级，不能小于50ms
func WithWatchDogExpireOption(watchDogExpire time.Duration) withLockOption {
	return func(d *Mutex) {
		d.watchDogExpire = watchDogExpire
	}
}

// WithWatchDogMsxTimesOption watchDogMaxTimes 最大续期次数，默认0，0表示无限次
// 只有调用WithWatchDogExpireOption启动后才生效
func WithWatchDogMsxTimesOption(watchDogMaxTimes int) withLockOption {
	return func(d *Mutex) {
		d.watchDogMaxTimes = watchDogMaxTimes
	}
}

// WithLogOption 设置日志
func WithLogOption(log util.Logger) withLockOption {
	return func(d *Mutex) {
		d.log = log
	}
}

// WithSpinIntervalOption 设置自旋间隔时间，默认为50ms
func WithSpinIntervalOption(spin time.Duration) withLockOption {
	return func(d *Mutex) {
		d.spinInterval = spin
	}
}

// NewDMutex 创建分布式锁
// key 锁的key，不能为空
func NewDMutex(key string, client *redis.Client, opts ...withLockOption) *Mutex {
	m := &Mutex{
		key:          key,
		client:       client,
		spinInterval: 50 * time.Millisecond,
	}
	for _, opt := range opts {
		if opt != nil {
			opt(m)
		}
	}
	if m.isOpenLocalLock {
		// 使用了本地锁优先模式，则看门狗不在有意义，这里强制取消它
		m.watchDogExpire = 0
	}

	return m
}

// TryLock 尝试加锁，非阻塞
// expire 支持到毫秒级
// holder：err 为nil时holder必不为nil
// err：nil-表示未发生异常，ErrLockMutex-表示与redis交互正常，但抢占锁失败
func (m *Mutex) TryLock(ctx context.Context, expire time.Duration) (*Holder, error) {
	if m.key == "" {
		return nil, ErrEmptyKey
	}
	if m.watchDogExpire > 0 { //启动看门狗
		if m.watchDogExpire < 50*time.Millisecond {
			return nil, ErrWatchDogExpireTooSmall
		}
	}
	randomValue := m.generateRandomValue()
	cmdRes := m.client.SetNX(ctx, m.key, randomValue, expire)
	isSuccess, err := cmdRes.Result()
	if err != nil {
		return nil, err
	}
	if !isSuccess {
		return nil, ErrLockMutex
	}

	//抢锁成功
	h := &Holder{
		randomValue: randomValue,
		mu:          m,
		expire:      expire,
	}
	// 启动看门狗
	if m.watchDogExpire > 0 {
		var watchCtx context.Context
		watchCtx, h.cancelFunc = context.WithCancel(context.Background())
		go func() {
			h.startWatchDog(watchCtx)
		}()
	}
	return h, nil
}

type withLockOption func(*Mutex)

// getSpinInterval 获取自旋间隔时间
func (m *Mutex) getSpinInterval() time.Duration {
	if m.spinInterval <= 0 {
		// 默认自旋锁50ms
		m.spinInterval = 50 * time.Millisecond
	}
	return m.spinInterval
}

// Lock 阻塞加锁，直到超时或ctx取消
// expire 支持到毫秒级
func (m *Mutex) Lock(ctx context.Context, expire time.Duration, timeout time.Duration) (h *Holder, err error) {
	// 指定了最大自旋时间
	if timeout > 0 {
		var cancelFunc func()
		ctx, cancelFunc = context.WithTimeout(ctx, timeout)
		defer cancelFunc()
	}
	if m.isOpenLocalLock {
		// 启动了本地优先锁
		local := getLocalLock(m.key)
		select {
		case <-local.c: // 取锁成功
		case <-ctx.Done(): // 超时或取消
			local.quoteCnt.Add(-1) // 未取到锁，引用计数减1
			return nil, ErrLockTimeout
		}

		defer func() {
			if err != nil { //取到锁，出错了要自动释放锁，未出错则交由调用方
				releaseLocalLock(m.key)
			}
		}()
	}

	h, err = m.TryLock(ctx, expire)

	if err == nil {
		return h, nil
	}
	if !errors.Is(err, ErrLockMutex) {
		return nil, err
	}
	// 加锁失败，自旋尝试
	ticker := time.NewTicker(m.getSpinInterval())
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			// 超时
			return nil, ErrLockTimeout
		case <-ticker.C:
			// 重新尝试加锁
			h, err = m.TryLock(context.Background(), expire)
			if err == nil {
				// 成功
				return h, nil
			}
			if !errors.Is(err, ErrLockMutex) {
				// 发生错误
				return nil, err
			}
		}
	}
}

// unlock 释放锁
// err: 不为nil表示操作异常
// int：受影响的行数，0表示没有要时放的
func (m *Mutex) unlock(ctx context.Context, randomValue int64) (int, error) {
	n, err := unlockScript.Run(ctx, m.client, []string{m.key}, randomValue).Int()
	if m.isOpenLocalLock {
		releaseLocalLock(m.key)
	}
	return n, err
}

// generateRandomValue 生成随机值
func (m *Mutex) generateRandomValue() int64 {
	return time.Now().UnixNano()
}
